// Copyright (C) 2016 Jochen Becher
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0

#include "modelindexer.h"

#include "modeleditor_constants.h"

#include "qmt/infrastructure/exceptions.h"
#include "qmt/infrastructure/uid.h"
#include "qmt/model/mdiagram.h"
#include "qmt/model/mpackage.h"
#include "qmt/model_controller/mvoidvisitor.h"
#include "qmt/project/project.h"
#include "qmt/serializer/projectserializer.h"
#include "qmt/tasks/findrootdiagramvisitor.h"

#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include <projectexplorer/projectnodes.h>

#include <utils/algorithm.h>
#include <utils/mimeutils.h>

#include <QDateTime>
#include <QLoggingCategory>
#include <QPointer>

using namespace ProjectExplorer;
using namespace Utils;

namespace ModelEditor::Internal {

Q_LOGGING_CATEGORY(log, "qtc.modeleditor.modelindexer", QtWarningMsg)

class QueuedFile
{
    friend size_t qHash(const QueuedFile &queuedFile);
    friend bool operator==(const QueuedFile &lhs, const QueuedFile &rhs);

public:
    QueuedFile() = default;

    QueuedFile(const QString &file, Project *project)
        : m_file(file)
        , m_project(project)
    {}

    QueuedFile(const QString &file, Project *project, const QDateTime &lastModified)
        : m_file(file)
        , m_project(project)
        , m_lastModified(lastModified)
    {}

    bool isValid() const { return !m_file.isEmpty() && m_project; }
    QString file() const { return m_file; }
    Project *project() const { return m_project; }
    QDateTime lastModified() const { return m_lastModified; }

private:
    QString m_file;
    Project *m_project = nullptr;
    QDateTime m_lastModified;
};

bool operator==(const QueuedFile &lhs, const QueuedFile &rhs)
{
    return lhs.m_file == rhs.m_file && lhs.m_project == rhs.m_project;
}

size_t qHash(const QueuedFile &queuedFile)
{
    return qHash(queuedFile.m_project) + qHash(queuedFile.m_project);
}

class IndexedModel
{
public:
    IndexedModel(const QString &modelFile, const QDateTime &lastModified)
        : m_modelFile(modelFile)
        , m_lastModified(lastModified)
    {}

    void reset(const QDateTime &lastModified)
    {
        m_lastModified = lastModified;
        m_modelUid = qmt::Uid::invalidUid();
        m_diagrams.clear();
    }

    QString file() const { return m_modelFile; }
    QDateTime lastModified() const { return m_lastModified; }
    QSet<Project *> owningProjects() const { return m_owningProjects; }
    void addOwningProject(Project *project) { m_owningProjects.insert(project); }
    void removeOwningProject(Project *project)
    {
        m_owningProjects.remove(project);
    }
    qmt::Uid modelUid() const { return m_modelUid; }
    void setModelUid(const qmt::Uid &modelUid) { m_modelUid = modelUid; }
    QSet<qmt::Uid> diagrams() const { return m_diagrams; }
    void addDiagram(const qmt::Uid &diagram) { m_diagrams.insert(diagram); }

private:
    QString m_modelFile;
    QDateTime m_lastModified;
    QSet<Project *> m_owningProjects;
    qmt::Uid m_modelUid;
    QSet<qmt::Uid> m_diagrams;
};

class IndexedDiagramReference
{
public:
    IndexedDiagramReference(const QString &file, const QDateTime &lastModified)
        : m_file(file)
        , m_lastModified(lastModified)
    {}

    void reset(const QDateTime &lastModified)
    {
        m_lastModified = lastModified;
        m_modelUid = qmt::Uid::invalidUid();
        m_diagramUid = qmt::Uid::invalidUid();
    }

    QString file() const { return m_file; }
    QDateTime lastModified() const { return m_lastModified; }
    QSet<Project *> owningProjects() const { return m_owningProjects; }
    void addOwningProject(Project *project) { m_owningProjects.insert(project); }
    void removeOwningProject(Project *project)
    {
        m_owningProjects.remove(project);
    }
    qmt::Uid modelUid() const { return m_modelUid; }
    void setModelUid(const qmt::Uid &modelUid) { m_modelUid = modelUid; }
    qmt::Uid diagramUid() const { return m_diagramUid; }
    void setDiagramUid(const qmt::Uid &diagramUid) { m_diagramUid = diagramUid; }

private:
    QString m_file;
    QDateTime m_lastModified;
    QSet<Project *> m_owningProjects;
    qmt::Uid m_modelUid;
    qmt::Uid m_diagramUid;
};

class DiagramsCollectorVisitor : public qmt::MVoidConstVisitor
{
public:
    DiagramsCollectorVisitor(IndexedModel *indexedModel)
        : qmt::MVoidConstVisitor()
        , m_indexedModel(indexedModel)
    {}

    void visitMObject(const qmt::MObject *object) final;
    void visitMDiagram(const qmt::MDiagram *diagram) final;

private:
    IndexedModel *m_indexedModel;
};

void DiagramsCollectorVisitor::visitMObject(const qmt::MObject *object)
{
    for (const qmt::Handle<qmt::MObject> &child : object->children()) {
        if (child.hasTarget())
            child.target()->accept(this);
    }
    visitMElement(object);
}

void DiagramsCollectorVisitor::visitMDiagram(const qmt::MDiagram *diagram)
{
    qCDebug(log) << "add diagram " << diagram->name() << " to index";
    m_indexedModel->addDiagram(diagram->uid());
    visitMObject(diagram);
}

class ModelIndexerPrivate
{
public:
    ~ModelIndexerPrivate()
    {
        QMT_CHECK(filesQueue.isEmpty());
        QMT_CHECK(queuedFilesSet.isEmpty());
        QMT_CHECK(indexedModels.isEmpty());
        QMT_CHECK(indexedModelsByUid.isEmpty());
        QMT_CHECK(indexedDiagramReferences.isEmpty());
        QMT_CHECK(indexedDiagramReferencesByDiagramUid.isEmpty());
    }

    QList<QueuedFile> filesQueue;
    QSet<QueuedFile> queuedFilesSet;
    QSet<QueuedFile> defaultModelFiles;

    QHash<QString, IndexedModel *> indexedModels;
    QHash<qmt::Uid, QSet<IndexedModel *> > indexedModelsByUid;

    QHash<QString, IndexedDiagramReference *> indexedDiagramReferences;
    QHash<qmt::Uid, QSet<IndexedDiagramReference *> > indexedDiagramReferencesByDiagramUid;
};

void ModelIndexer::handleQueuedFiles()
{
    while (!d->filesQueue.isEmpty()) {
        QueuedFile queuedFile = d->filesQueue.takeFirst();
        d->queuedFilesSet.remove(queuedFile);
        qCDebug(log) << "handle queued file " << queuedFile.file()
                        << "from project " << queuedFile.project()->displayName();

        bool scanModel = false;
        IndexedModel *indexedModel = d->indexedModels.value(queuedFile.file());
        if (!indexedModel) {
            qCDebug(log) << "create new indexed model";
            indexedModel = new IndexedModel(queuedFile.file(),
                                            queuedFile.lastModified());
            indexedModel->addOwningProject(queuedFile.project());
            d->indexedModels.insert(queuedFile.file(), indexedModel);
            scanModel = true;
        } else if (queuedFile.lastModified() > indexedModel->lastModified()) {
            qCDebug(log) << "update indexed model";
            indexedModel->addOwningProject(queuedFile.project());
            indexedModel->reset(queuedFile.lastModified());
            scanModel = true;
        }
        if (scanModel) {
            // load model file
            qmt::ProjectSerializer projectSerializer;
            qmt::Project project;
            try {
                projectSerializer.load(FilePath::fromString(queuedFile.file()), &project);
            } catch (const qmt::Exception &e) {
                qWarning() << e.errorMessage();
                return;
            }
            indexedModel->setModelUid(project.uid());
            // add indexedModel to set of indexedModelsByUid
            QSet<IndexedModel *> indexedModels = d->indexedModelsByUid.value(project.uid());
            indexedModels.insert(indexedModel);
            d->indexedModelsByUid.insert(project.uid(), indexedModels);
            // collect all diagrams of model
            DiagramsCollectorVisitor visitor(indexedModel);
            project.rootPackage()->accept(&visitor);
            if (d->defaultModelFiles.remove(queuedFile)) {
                // check if model has a diagram which could be opened
                qmt::FindRootDiagramVisitor diagramVisitor;
                project.rootPackage()->accept(&diagramVisitor);
                if (diagramVisitor.diagram())
                    emit openDefaultModel(project.uid());
            }
        }
    }
}

ModelIndexer::ModelIndexer(QObject *parent)
    : QObject(parent)
    , d(new ModelIndexerPrivate)
{
    connect(ProjectManager::instance(), &ProjectManager::projectAdded,
            this, &ModelIndexer::onProjectAdded);
    connect(ProjectManager::instance(), &ProjectManager::aboutToRemoveProject,
            this, &ModelIndexer::onAboutToRemoveProject);
}

ModelIndexer::~ModelIndexer()
{
    delete d;
}

QString ModelIndexer::findModel(const qmt::Uid &modelUid)
{
    QSet<IndexedModel *> indexedModels = d->indexedModelsByUid.value(modelUid);
    if (indexedModels.isEmpty())
        return {};
    IndexedModel *indexedModel = *indexedModels.cbegin();
    QMT_ASSERT(indexedModel, return {});
    return indexedModel->file();
}

QString ModelIndexer::findDiagram(const qmt::Uid &modelUid, const qmt::Uid &diagramUid)
{
    Q_UNUSED(modelUid) // avoid warning in release mode

    QSet<IndexedDiagramReference *> indexedDiagramReferences = d->indexedDiagramReferencesByDiagramUid.value(diagramUid);
    if (indexedDiagramReferences.isEmpty())
        return QString();
    IndexedDiagramReference *indexedDiagramReference = *indexedDiagramReferences.cbegin();
    QMT_ASSERT(indexedDiagramReference, return QString());
    QMT_ASSERT(indexedDiagramReference->modelUid() == modelUid, return QString());
    return indexedDiagramReference->file();
}

void ModelIndexer::onProjectAdded(Project *project)
{
    connect(project, &Project::fileListChanged, this, [this, p = QPointer(project)] {
        if (p)
            onProjectFileListChanged(p.data());
    }, Qt::QueuedConnection);
    scanProject(project);
}

void ModelIndexer::onAboutToRemoveProject(Project *project)
{
    disconnect(project, &Project::fileListChanged, this, nullptr);
    forgetProject(project);
}

void ModelIndexer::onProjectFileListChanged(Project *project)
{
    scanProject(project);
}

void ModelIndexer::scanProject(Project *project)
{
    if (!project->rootProjectNode())
        return;

    // TODO harmonize following code with findFirstModel()?
    const FilePaths files = project->files(Project::SourceFiles);
    QList<QueuedFile> filesQueue;
    QSet<QueuedFile> filesSet;

    const MimeType modelMimeType = Utils::mimeTypeForName(Constants::MIME_TYPE_MODEL);
    if (modelMimeType.isValid()) {
        for (const FilePath &file : files) {
            if (modelMimeType.suffixes().contains(file.completeSuffix())) {
                QueuedFile queuedFile(file.toUrlishString(), project, file.lastModified());
                filesQueue.append(queuedFile);
                filesSet.insert(queuedFile);
            }
        }
    }

    // FIXME: This potentially iterates over all files again.
    QString defaultModelFile = findFirstModel(project->rootProjectNode(), modelMimeType);

    bool filesAreQueued = false;
    {
        // remove deleted files from queue
        for (int i = 0; i < d->filesQueue.size();) {
            if (d->filesQueue.at(i).project() == project) {
                if (filesSet.contains(d->filesQueue.at(i))) {
                    ++i;
                } else {
                    d->queuedFilesSet.remove(d->filesQueue.at(i));
                    d->filesQueue.removeAt(i);
                }
            }
        }

        // remove deleted files from indexed models
        const QStringList files = d->indexedModels.keys();
        for (const QString &file : files) {
            if (!filesSet.contains(QueuedFile(file, project)))
                removeModelFile(file, project);
        }

        // remove deleted files from indexed diagrams
        const QStringList deletedFiles = d->indexedDiagramReferences.keys();
        for (const QString &file : deletedFiles) {
            if (!filesSet.contains(QueuedFile(file, project)))
                removeDiagramReferenceFile(file, project);
        }

        // queue files
        while (!filesQueue.isEmpty()) {
            QueuedFile queuedFile = filesQueue.takeFirst();
            if (Utils::insert(d->queuedFilesSet, queuedFile)) {
                QMT_CHECK(!d->filesQueue.contains(queuedFile));
                d->filesQueue.append(queuedFile);
                filesAreQueued = true;
            }
        }

        // auto-open model file only if project is already configured
        if (!defaultModelFile.isEmpty() && !project->targets().isEmpty()) {
            d->defaultModelFiles.insert(QueuedFile(defaultModelFile, project, QDateTime()));
        }
    }

    if (filesAreQueued)
        handleQueuedFiles();
}

QString ModelIndexer::findFirstModel(FolderNode *folderNode, const MimeType &mimeType)
{
    if (!mimeType.isValid())
        return QString();

    const QStringList suffixes = mimeType.suffixes();
    FileNode *foundFileNode = folderNode->findChildFileNode([&](FileNode *fn) {
        return suffixes.contains(fn->filePath().completeSuffix());
    });
    if (foundFileNode)
         return foundFileNode->filePath().toUrlishString();

    QString modelFileName;
    folderNode->findChildFolderNode([&](FolderNode *fn) {
        modelFileName = findFirstModel(fn, mimeType);
        return !modelFileName.isEmpty();
    });
    return modelFileName;
}

void ModelIndexer::forgetProject(Project *project)
{
    const FilePaths files = project->files(Project::SourceFiles);

    for (const FilePath &file : files) {
        const QString fileString = file.toUrlishString();
        // remove file from queue
        QueuedFile queuedFile(fileString, project);
        if (d->queuedFilesSet.remove(queuedFile)) {
            QMT_CHECK(d->filesQueue.contains(queuedFile));
            d->filesQueue.removeOne(queuedFile);
            QMT_CHECK(!d->filesQueue.contains(queuedFile));
        }
        removeModelFile(fileString, project);
        removeDiagramReferenceFile(fileString, project);
    }
}

void ModelIndexer::removeModelFile(const QString &file, Project *project)
{
    IndexedModel *indexedModel = d->indexedModels.value(file);
    if (indexedModel && indexedModel->owningProjects().contains(project)) {
        qCDebug(log) << "remove model file " << file
                        << " from project " << project->displayName();
        indexedModel->removeOwningProject(project);
        if (indexedModel->owningProjects().isEmpty()) {
            qCDebug(log) << "delete indexed model " << project->displayName();
            d->indexedModels.remove(file);

            // remove indexedModel from set of indexedModelsByUid
            QMT_CHECK(d->indexedModelsByUid.contains(indexedModel->modelUid()));
            QSet<IndexedModel *> indexedModels = d->indexedModelsByUid.value(indexedModel->modelUid());
            QMT_CHECK(indexedModels.contains(indexedModel));
            indexedModels.remove(indexedModel);
            if (indexedModels.isEmpty())
                d->indexedModelsByUid.remove(indexedModel->modelUid());
            else
                d->indexedModelsByUid.insert(indexedModel->modelUid(), indexedModels);

            delete indexedModel;
        }
    }
}

void ModelIndexer::removeDiagramReferenceFile(const QString &file, Project *project)
{
    IndexedDiagramReference *indexedDiagramReference = d->indexedDiagramReferences.value(file);
    if (indexedDiagramReference) {
        QMT_CHECK(indexedDiagramReference->owningProjects().contains(project));
        qCDebug(log) << "remove diagram reference file "
                        << file << " from project " << project->displayName();
        indexedDiagramReference->removeOwningProject(project);
        if (indexedDiagramReference->owningProjects().isEmpty()) {
            qCDebug(log) << "delete indexed diagram reference from " << file;
            d->indexedDiagramReferences.remove(file);

            // remove indexedDiagramReference from set of indexedDiagramReferecesByDiagramUid
            QMT_CHECK(d->indexedDiagramReferencesByDiagramUid.contains(indexedDiagramReference->diagramUid()));
            QSet<IndexedDiagramReference *> indexedDiagramReferences = d->indexedDiagramReferencesByDiagramUid.value(indexedDiagramReference->diagramUid());
            QMT_CHECK(indexedDiagramReferences.contains(indexedDiagramReference));
            indexedDiagramReferences.remove(indexedDiagramReference);
            if (indexedDiagramReferences.isEmpty()) {
                d->indexedDiagramReferencesByDiagramUid.remove(
                            indexedDiagramReference->diagramUid());
            } else {
                d->indexedDiagramReferencesByDiagramUid.insert(
                            indexedDiagramReference->diagramUid(), indexedDiagramReferences);
            }

            delete indexedDiagramReference;
        }
    }
}

} // namespace ModelEditor::Internal
